'''
XML Config Parser. An XML parser for config files.
Copyright (C) 2006  Jonathan Claggett

This library is free software; you can redistribute it and/or modify it under
the terms of the GNU Lesser General Public License as published by the Free
Software Foundation; either version 2.1 of the License, or (at your option) any
later version.

This library is distributed in the hope that it will be useful, but WITHOUT ANY
WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A
PARTICULAR PURPOSE.  See the GNU Lesser General Public License for more
details.

You should have received a copy of the GNU Lesser General Public License along
with this library; if not, write to the Free Software Foundation, Inc., 51
Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA

You may contact the author at <jonathan@claggett.org>
'''
if __debug__: import pdb

#import xml.parsers.expat as XML
import _xmlplus.parsers.expat as XML
import type
from pattern import Pattern, All, Any, Choice, Seq

class Reader(object):
    '''
    The XML Reader that uses the Element class tree to read the proper XML.
    Callbacks used with the Element class include the start(), char(), and
    end() methods.
    '''

    def __init__(self, element):
        # Define a trivial root builder element which contains the passed in
        # element as its only child.
        class Root(Element):
            contents = Choice(element)
        self.element = self.root = Root(read_contents=self.read_contents)

        # Create a casting object
        #self.cast = cast.Cast()

        # Create the expat parser and hook up the needed callbacks.
        self.parser = XML.ParserCreate()
        self.parser.StartElementHandler = self.start_element_handler
        self.parser.CharacterDataHandler = self.character_data_handler
        self.parser.EndElementHandler = self.end_element_handler
        self.parser_suspended = False # State of the parser.

    def read_text(self, text, isfinal = True):
        # Create an initial replacement for the standard resume_parser function
        # to be called just once.
        true_resume_parser = self.resume_parser
        def init_resume_parser():
            self.resume_parser = true_resume_parser
            return self.parser.Parse(text, isfinal)
        self.resume_parser = init_resume_parser

        self.element.read_contents()
        return self.element.model

    def read_file(self, file):
        # Create an initial replacement for the standard resume_parser function
        # to be called just once.
        true_resume_parser = self.resume_parser
        def init_resume_parser():
            self.resume_parser = true_resume_parser
            return self.parser.ParseFile(file)
        self.resume_parser = init_resume_parser

        self.element.read_contents()
        return self.element.model

    def read_contents(self):
        while not self.element.end:
            if self.resume_parser() != XML.XML_STATUS_SUSPENDED:
                self.element.end = True

            elif self.element.start:
                self.element.start = False
                self.element.read( **dict( [ (str(k), self.cast(v))
                    for k, v in self.element.attrs.items() ] ) )
                self.read_contents() # Make sure read_contents is called.
                self.element = self.element.parent

    def start_element_handler(self, name, attrs):
        self.element = self.element.get_child(name)
        self.element.attrs = attrs
        self.suspend_parser()

    def character_data_handler(self, text):
        self.element.char(text)

    def end_element_handler(self, name):
        self.element.end = True
        self.suspend_parser()

    def suspend_parser(self):
        if self.parser_suspended:
            return
        self.parser_suspended = True
        self.parser.StopParser()

    def resume_parser(self):
        self.parser_suspended = False
        return self.parser.ResumeParser()

class Writer(object):
    '''
    The XML Writer that uses the Element class tree to generate the proper XML.
    Callbacks used with the Element class include the write() method.
    '''
    def __init__(self, element):

        # Define a trivial root element which contains the passed in element
        # as its only child.
        class Root(Element):
            contents = Choice(element)
        self.element = self.root = Root()

    def write_text(self, model):
        text = ''
        #element = self.root(model=model)
        #element.write()
        return text

    def write_file(self, file, model):
        return True

# Base element object 
class Element(object):
    '''
    Element is the parent class of Element classes used to handle specific XML
    elements.
    '''

    contents = Any()

    def __init__(self, parent=None, model=None, read_contents=None):
        self.parent = parent # Parent builder
        self.model = model # Pointer to the relevent section of the model.

        self.read_contents = read_contents # method for parsing contents.
        self.start = True
        self.end = False

    def get_child(self, name):
        return self.contents.find(name)(
            parent=self,
            model=self.model,
            read_contents=self.read_contents)

    @classmethod
    def get_name(cls):
        if hasattr(cls, 'name'):
            return cls.name
        else:
            return cls.__name__.lower()

    @classmethod
    def read_text(cls, text):
        '''
        Read an XML text string using the given Element class as the root
        class. Returns a populated model based on the XML text string.
        '''
        return Reader(cls).read_text(text)

    @classmethod
    def read_file(cls, file):
        '''
        Read an XML file using the given Element class as the root class.
        Returns the populated model based on the contents of the XML file.
        '''
        return Reader(cls).read_file(file)

    @classmethod
    def write_text(cls, model):
        '''
        Write the model as an XML text string using the given Element class as
        the root class. Returns the XML text string.
        '''
        return Writer(cls).write_text(model)

    @classmethod
    def write_file(cls, file, model):
        '''
        Write the model as an XML file with the specified file name using the
        given Element class as the root class. Returns True or False based on
        the success of writing to the file.
        '''
        return Writer(cls).write_file(file, model)

    @staticmethod
    def write(model): pass

    @staticmethod
    def start(**attrs): pass

    @staticmethod
    def char(text): pass

    @staticmethod
    def end(): pass

# Proposed 0.2 API
class PrintDoc(Document):
    element = Print

    def read(self):
        prolog = self.read_prolog()
        model = self.read_element()

    def write(self):
        pass

class Print(Element):
    name = 'print'

    attrs = Attrs(
        newline = Attr(type.String | type.Bool, default=True),
        format = Attr(type.Bool, default=NoDefault) )

    contents = All(), Any(), Choice(), Seq(), Pattern(), CData(),

    def read(self, pmodel):
        attrs = self.read_attrs()
        value = self.read_contents() # when contents = CData
        self.read_contents() # for everything else

    def write(self):
        pass

# Unit Testing
if __debug__:
    import unittest

    class TestElement(unittest.TestCase):

        def test_trivial(self):

            class Print(Element):
                attrs = dict(
                    newline = Attr(type.BOOL, default = False) )
                contents = CData()

                def read(self, model):
                    model.text = self.read_contents()
                    if self.read_attrs().newline:
                        model.text += '\n'

                def write(self, text):
                    if text[-1] == '\n':
                        self.write_attrs(newline = True)
                        text = text[:-1]
                    self.write_cdata(text)

            text = u'<print newline="true">Hello World!</print>'
            model = Print.read_text(text)
            self.assertEqual(model.text, u'''Hello World!\n''')

            new_text = Print.write_text(model)
            #self.assertEqual(text, new_text)

            model = Print.read_text( u'<print>Hello World!</print>' )
            self.assertEqual(model.text, u'''Hello World!''')

        def test_easy(self):
            class Book(Element):
                def read(self, title = 'Unknown Book', auto=False):
                    if auto: self.parent.model += '(Autobiography) '
                    self.parent.model += title + '\n'
                def char(self, text):
                    self.parent.model += '\t' + text + '\n'

            class Catalog(Element):
                name = 'catalog'
                contents = Any(Book)
                def read(self, title = 'Unknown Catalog'):
                    self.title = title
                    self.model = '\nBegin Catalog: ' + self.title + '\n\n'
                    self.read_contents()
                    self.model += '\nEnd Catalog: ' + self.title

            model = Catalog.read_text( text = u'''
                <catalog title="An Incomplete Catalog of Penguin Books">
                <book title="Illiad, The" auto="TruE">ipsum lorem</book>
                <book title="Swallows and Amazons">ipsum lorem</book>
                </catalog>
                ''')

            self.assertEqual( model, u'''
Begin Catalog: An Incomplete Catalog of Penguin Books

(Autobiography) Illiad, The
	ipsum lorem
Swallows and Amazons
	ipsum lorem

End Catalog: An Incomplete Catalog of Penguin Books''')

    if __name__ == '__main__':
        unittest.main()
        #test_support.run_unittest( cast.TestCast, TestParser )

# vim: ai et sw=4 ts=4
